package com.basker.pisces.springcloud.feign.fallback;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.net.ConnectException;
import java.net.SocketTimeoutException;
import java.util.concurrent.RejectedExecutionException;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.basker.pisces.exception.ExceptionFactory;
import com.basker.pisces.resource.StringResources;
import com.basker.pisces.service.constant.ServiceConstants;
import com.basker.pisces.service.dto.IServiceResult;
import com.basker.pisces.service.dto.ServiceResultFactory;
import com.netflix.client.ClientException;
import com.netflix.hystrix.HystrixCircuitBreaker;
import com.netflix.hystrix.HystrixCommandKey;
import com.netflix.hystrix.exception.HystrixTimeoutException;

import feign.Feign;
import feign.FeignException;

/**
 * fallback动态代理的处理器，用于处理导致的fallback的异常，并合法地返回值
 *
 * @author hangwen
 */
public class FallbackInvocationHandler implements InvocationHandler {

    private static final Logger logger = LoggerFactory.getLogger(FallbackInvocationHandler.class);

    private Class<?> clientType;
    private Throwable cause;

    public FallbackInvocationHandler(Class<?> clientType, Throwable cause) {
        this.clientType = clientType;
        this.cause = cause;
    }

    public Throwable getCause() {
        return cause;
    }

    @Override
    public final Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        if (method.getDeclaringClass().equals(Object.class)) {
            return method.invoke(proxy, args);
        }

        try {
            CauseReason reason = resolveCauseReason(method);

            if (logger.isErrorEnabled()) {
                logger.error(reason.asString(), this.cause);
            }

            return this.getReturnValue(method, reason);
        } catch (Throwable e) {
            String configKey = Feign.configKey(this.clientType, method);

            if (logger.isErrorEnabled()) {
                logger.error(StringResources.get("pisces-feign.FallbackInvocationHandler.invoke-001", configKey,
                        e.getMessage()));
            }

            if (e instanceof RuntimeException) {
                throw e;
            }

            throw new RuntimeException(e);
        }
    }

    public void setCause(Throwable cause) {
        this.cause = cause;
    }

    protected Object getReturnValue(Method method, CauseReason reason) {
        Class<?> returnType = method.getReturnType();
        if (IServiceResult.class.isAssignableFrom(returnType)) {
            return ServiceResultFactory.build(returnType, reason.getStatus(), reason.getMessage(), null);
        }

        throw ExceptionFactory.createRuntimeException("pisces-feign.FallbackInvocationHandler.getReturnValue-001",
                Feign.configKey(this.clientType, method), returnType.getName());
    }

    private boolean circuitBreakerIsOpen(Method method) {
        String configKey = Feign.configKey(this.clientType, method);
        HystrixCommandKey key = HystrixCommandKey.Factory.asKey(configKey);
        HystrixCircuitBreaker breaker = HystrixCircuitBreaker.Factory.getInstance(key);

        return breaker != null ? breaker.isOpen() : false;
    }

    private Throwable findCause(Throwable cause) {
        if (cause.getCause() == null) {
            return cause;
        }

        if (cause.getCause().equals(cause)) {
            return cause;
        }

        return this.findCause(cause.getCause());
    }

    private CauseReason resolveCauseReason(Method method) {
        String reasonCode = ServiceConstants.Fallback.ReasonCode.CLIENT_INVOKE_EXCEPTION;
        String message = cause.getMessage();

        Throwable realCause = this.findCause(this.cause);
        String stringResoucePrefix = "pisces-feign.FallbackInvocationHandler.resolveMessage-";
        if (realCause instanceof ConnectException) {
            // 连接异常，比如服务器宕机，网络断开
            reasonCode = ServiceConstants.Fallback.ReasonCode.CONNECT_ERROR;
            message = StringResources.get(stringResoucePrefix + "008", message);
        } else if (realCause instanceof SocketTimeoutException) {
            // 超时
            reasonCode = ServiceConstants.Fallback.ReasonCode.SOCKET_READ_TIMEOUT;
            message = StringResources.get(stringResoucePrefix + "001", message);
        } else {
            String realCauseMessage = realCause.getMessage();
            if (realCause instanceof ClientException) {
                // 没有服务提供者
                if (realCauseMessage.startsWith("Load balancer does not have available server for client")) {
                    reasonCode = ServiceConstants.Fallback.ReasonCode.NO_AVAILABLE_SERVER;

                    String serviceName = realCauseMessage.split(":")[1].trim();
                    message = StringResources.get(stringResoucePrefix + "002", serviceName);
                }
            } else if (realCause instanceof HystrixTimeoutException) {
                // 熔断器超时
                reasonCode = ServiceConstants.Fallback.ReasonCode.HYSTRIX_TIMEOUT;
                message = StringResources.get(stringResoucePrefix + "003");
            } else if (realCause instanceof RejectedExecutionException) {
                // 线程池耗尽
                reasonCode = ServiceConstants.Fallback.ReasonCode.HYSTRIX_THREAD_POOL_FULL;
                message = StringResources.get(stringResoucePrefix + "004");
            } else if (realCause instanceof FeignException) {
                int status = ((FeignException) realCause).status();

                switch (status) {
                    case 404:
                        // 目标服务404
                        reasonCode = ServiceConstants.Fallback.ReasonCode.SERVICE_404;

                        message = StringResources.get(stringResoucePrefix + "006");
                        break;
                    default:
                        break;
                }

            } else if (realCause instanceof RuntimeException) {
                // 熔断器是否已经打开
                if (this.circuitBreakerIsOpen(method)) {
                    // 熔断器已打开
                    reasonCode = ServiceConstants.Fallback.ReasonCode.HYSTRIX_CIRCUITBREAKER_OPEN;
                    message = StringResources.get(stringResoucePrefix + "005");
                } else if ("could not acquire a semaphore for execution".equals(realCauseMessage)) {
                    reasonCode = ServiceConstants.Fallback.ReasonCode.HYSTRIX_SEMAPHORE_NOT_ACQUIRED;
                    message = StringResources.get(stringResoucePrefix + "007");
                }
            }
        }

        String configKey = Feign.configKey(this.clientType, method);
        String resolvedMessage = StringResources.get(stringResoucePrefix + "000", configKey, message);
        return new CauseReason(reasonCode, resolvedMessage);
    }

    public static class CauseReason {
        private Integer status = ServiceConstants.StatusCode.HYSTRIX_FALLBACK;
        private String reasonCode;
        private String message;

        public CauseReason(String reasonCode, String message) {
            this.reasonCode = reasonCode;
            this.message = message;
        }

        public String asString() {
            return this.toString();
        }

        public String getMessage() {
            return message;
        }

        public String getReasonCode() {
            return reasonCode;
        }

        public Integer getStatus() {
            return status;
        }

        public void setStatus(Integer status) {
            this.status = status;
        }

        @Override
        public String toString() {
            return this.reasonCode + "-" + this.message;
        }

    }

}
